package com.ek.kademlia;

import java.util.LinkedList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ek.common.Bucket;
import com.ek.common.RoutingTable;
import com.ek.util.BitwiseUtils;

public class RoutingTableThreadSafe implements RoutingTable {
    private static final transient Logger log = LoggerFactory.getLogger(RoutingTableThreadSafe.class);
    private static final int KBuckets_size = 160;
    private static final int KBucket_size = 20;
    private final byte[] nodeId;
    /**
     * Attention, Note that Tree is not synchronized
     */
    private final LinkedList<LinkedList<Bucket>> kBuckets = new LinkedList<LinkedList<Bucket>>();
    private final BitwiseUtils bitUtils = new BitwiseUtils();

    public RoutingTableThreadSafe(byte[] id) {
        this.nodeId = id;
        initializekBuckets(KBuckets_size);
    }

    /**
     * @see {@link com.ek.common.RoutingTable#addBucket(DefaultBucket)}
     */
    public boolean addBucket(DefaultBucket bucket) {
        synchronized (kBuckets) {
            int indexOfBucket = getIndexOfTargetBucket(nodeId, bucket.getNodeID());
            log.debug("add this Bucket:" + bucket + " at index:" + indexOfBucket);
            LinkedList<Bucket> kBucket = kBuckets.get(indexOfBucket);
            if (isKBucketFull(kBucket)) {
                insertIfFirstNotAlive(kBucket, bucket);
            } else {
                insertAtLast(kBucket, bucket);
            }
            log.debug("end add this Bucket:" + bucket);
        }
        return true;
    }

    /**
     * @see {@link com.ek.common.RoutingTable#getClosestBucket(byte[], int)}
     */
    public LinkedList<Bucket> getClosestBucket(byte[] key, int alpha) {
        log.debug("getClosestBucket");
        LinkedList<Bucket> result = new LinkedList<Bucket>();
        int indexOfTargetBucket = getIndexOfTargetBucket(nodeId, key);
        synchronized (kBuckets) {
            LinkedList<Bucket> target = kBuckets.get(indexOfTargetBucket);
            addAlpha(indexOfTargetBucket, result, alpha, true);
            if (result.size() != alpha) {
                addAlpha(indexOfTargetBucket - 1, result, alpha, false);
            }
        }
        log.debug("end getClosestBucket:");
        return result;
    }

    /**
     * @param indexOfTargetBucket
     * @param result
     * @param alpha
     * @param increment
     */
    protected void addAlpha(int indexOfTargetBucket, LinkedList<Bucket> result, int alpha, boolean increment) {
        if (indexOfTargetBucket == kBuckets.size() || indexOfTargetBucket == -1) {
            return;
        }
        LinkedList<Bucket> target = kBuckets.get(indexOfTargetBucket);
        int nextIndex = increment ? indexOfTargetBucket + 1 : indexOfTargetBucket - 1;
        if (target.size() == 0) {
            addAlpha(nextIndex, result, alpha, increment);

        } else if (target.size() >= alpha) {
            addAlphaFromTarget(target, result, alpha);

        } else if (target.size() > 0) {
            result.addAll(target);
            addAlpha(nextIndex, result, alpha - target.size(), increment);

        }
    }

    /**
     * @param target
     * @param result
     * @param alpha
     */
    protected void addAlphaFromTarget(LinkedList<Bucket> target, LinkedList<Bucket> result, int alpha) {
        for (int i = 0; i < alpha; i++) {
            result.addLast(target.get(target.size() - i - 1));
        }
    }

    /**
     * Inserts the bucket at last.
     * @param bucket
     * @param index
     */
    protected void insertAtLast(LinkedList<Bucket> kBucket, Bucket bucket) {
        synchronized (kBucket) {
            if (!kBucket.contains(bucket)) {
                kBucket.addLast(bucket);
            }
        }
    }

    /**
     * Inserts the Bucket in KBucket queue tail of index i, if the first Bucket is alive, do nothing
     * otherwise.
     * @param bucket
     * @param index
     */
    protected void insertIfFirstNotAlive(LinkedList<Bucket> kBucket, Bucket bucket) {
        synchronized (kBucket) {
            Bucket first = kBucket.getFirst();
            if (!isAlive(first)) {
                kBucket.removeFirst();
                if (!kBucket.contains(bucket)) {
                    kBucket.addLast(bucket);
                }
            }
        }

    }

    /**
     * Check if the KBucket is full.
     * @param index
     * @return
     */
    protected boolean isKBucketFull(LinkedList<Bucket> kBucket) {
        synchronized (kBucket) {
            return kBucket.size() == KBucket_size;
        }
    }

    /**
     * verify that bucket is alive.
     * @param first
     * @return
     */
    protected boolean isAlive(Bucket first) {
        return false;
    }

    /**
     * Returns the index of K-Bucket.
     * @param keyA
     * @param keyB
     * @return -1 if the keyA is equals to KeyB
     */
    protected int getIndexOfTargetBucket(byte[] keyA, byte[] keyB) {
        int length = 8 * keyA.length;
        int commonPrefix = bitUtils.commonPrefix(keyA, keyB);
        return length - commonPrefix - 1;
    }

    /**
     * useful for tests.
     * @return
     */
    protected LinkedList<LinkedList<Bucket>> getKBuckets() {
        return kBuckets;
    }

    /**
     * Initiates the kBuckets.
     * @param size
     */
    private void initializekBuckets(int size) {
        for (int i = 0; i < size; i++) {
            kBuckets.add(new LinkedList<Bucket>());
        }
    }

}
